Skip to content

feat(core): configure model reasoning capabilities - #10999

Open
callmeYe wants to merge 1 commit into
mainfrom
codex/provider-model-reasoning-config
Open

feat(core): configure model reasoning capabilities#10999
callmeYe wants to merge 1 commit into
mainfrom
codex/provider-model-reasoning-config

Conversation

@callmeYe

@callmeYe callmeYe commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds a declarative reasoning capability to existing provider model definitions and carries it through the current model registry into ACP, session restoration, workspace previews, TUI effort controls, and the final OpenAI-compatible request. The native deepseek-v4-pro entry is the only newly configured model: it exposes high and max, defaults to high, and disables reasoning with thinking.type = disabled.

Why it's needed

Adding reasoning support for a model should be a provider-configuration change, not another model-name or hostname branch spread across clients and request adapters. This establishes that contract once and uses one DeepSeek model to prove that future model additions can remain configuration-only.

Reviewer Test Plan

How to verify

Configure the native DeepSeek provider and select deepseek-v4-pro. Confirm that ACP/WebShell and both TUI effort pickers expose only high and max, with high selected by default. Select max and confirm the final request contains top-level reasoning_effort: "max" without a nested effort. Disable reasoning and confirm the request contains thinking: { "type": "disabled" }. Switch to a model without an explicit reasoning capability and confirm no new provider-specific behavior is inferred.

Evidence (Before & After)

Before: the generic effort control could offer tiers not native to the selected model, and model support required code-side provider knowledge. After: deepseek-v4-pro offers only its configured tiers and the same configuration determines the final wire fields. Automated coverage verifies capability propagation, invalid-tier fallback, disable payloads, session reconciliation, and TUI filtering.

Tested on

OS Status
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

Environment (optional)

Node.js 22; focused Vitest suites plus full build, typecheck, and lint.

Risk & Scope

  • Main risk or tradeoff: reasoning behavior now trusts explicit model capability metadata, so incorrect provider configuration would expose an incorrect tier or wire field.
  • Not validated / out of scope: Qwen 3.8, DeepSeek snapshots, Kimi, GLM, MiniMax, Step, Coding Plan, and live provider API calls. These model additions are intentionally deferred to a configuration-only follow-up.
  • Breaking changes / migration notes: none; models without an explicit capability retain existing behavior.

Linked Issues

Narrow successor to #10913 and #9590.

中文说明

本 PR 做了什么

在现有 provider 模型定义中加入声明式 reasoning capability,并通过既有模型注册表传递给 ACP、会话恢复、workspace preview、TUI effort 控件和最终 OpenAI-compatible 请求。本 PR 只为原生 deepseek-v4-pro 增加配置:仅展示 highmax,默认 high,关闭推理时发送 thinking.type = disabled

为什么需要

新增模型的 reasoning 支持应该只修改 provider 配置,而不应继续在客户端和请求适配器中散布模型名或 hostname 分支。本 PR 一次性建立该契约,并用一个 DeepSeek 模型证明后续模型可以仅通过配置接入。

Reviewer 测试计划

如何验证

配置原生 DeepSeek provider 并选择 deepseek-v4-pro。确认 ACP/WebShell 和两套 TUI effort picker 只展示 highmax,默认选择 high。选择 max 后确认最终请求包含顶层 reasoning_effort: "max" 且没有嵌套 effort。关闭 reasoning 后确认请求包含 thinking: { "type": "disabled" }。切换到没有显式 reasoning capability 的模型,确认不会推断新的 provider 专有行为。

Before / After 证据

Before:通用 effort 控件可能展示模型不原生支持的档位,新增模型还需要在代码中加入 provider 知识。After:deepseek-v4-pro 只展示配置声明的档位,同一配置决定最终 wire 字段。自动化测试覆盖 capability 透传、非法档位回落、关闭 payload、会话重校验和 TUI 过滤。

测试环境

macOS + Node.js 22;聚焦 Vitest、完整 build、typecheck 和 lint 均通过。Windows 和 Linux 未本地验证。

风险与范围

  • 主要风险:reasoning 行为依赖显式模型 capability;错误配置可能暴露错误档位或 wire 字段。
  • 未验证 / 不包含:Qwen 3.8、DeepSeek 快照、Kimi、GLM、MiniMax、Step、Coding Plan 和真实 provider API 调用;这些模型将在后续仅配置 PR 中加入。
  • 破坏性变更 / 迁移:无;没有显式 capability 的模型保持现有行为。

关联

#10913#9590 的精简继任实现。

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR — the write-up and the design doc are unusually clear about intent, which made this easy to follow.

Template looks good ✓ (all required headings present, Tested-on filled in, Chinese section included).

Problem: this is a feat, so no reproduction is expected, and the UX observation behind it is real — today /effort on deepseek-v4-pro offers low/medium/xhigh, which that model does not accept. But the framing that reasoning support currently requires "another model-name or hostname branch spread across clients and request adapters" does not match what is on main. A declarative, per-model contract already exists in the provider adapter layer:

  • supportedReasoningEffortsFor(model) on OpenAICompatibleProvider — its docstring is literally "Effort tiers this endpoint accepts for model. Override in a subclass whose endpoint takes max". deepseek.ts and zai.ts already override it, and DashScope declares DASHSCOPE_TIERED_EFFORTS.
  • clampConfiguredReasoningEffort() caps the pipeline-injected tier through clampReasoningEffort(requested, supported) in core/reasoning-effort.ts, whose header says the ladder exists "so a new provider only needs to declare its supported subset".
  • translateReasoningEffort() in deepseek.ts already flattens nested reasoning.effort onto top-level reasoning_effort, including xhigh → max, and the pipeline already emits thinking: { type: 'disabled' } for DeepSeek hosts.
  • Config.setReasoningEffort's own docstring states the invariant: "Provider adapters clamp the tier to what the active model supports."

So the question this PR raises is not "should models declare their reasoning capability" (they should) but where that declaration lives: in the provider preset / model registry (this PR), or in the provider adapter (today). After this PR both mechanisms are live at once, and they disagree.

Direction: escalating to a maintainer. This touches model selection, adds a new user-editable settings contract (modelProviders[].capabilities.reasoning), and changes the shape of the ACP reasoning_effort session-config option (option list + _meta keys). Those are the areas where the gate does not get to decide architecture on its own. Relatedly, the core change is 105 production lines inside packages/core/src/{models,providers,core}, which puts it under the 100%-confidence bar for core — and I am not at 100%, for the concrete reasons below.

Size: 534 changed lines total → 368 production (core 105, cli 263), 126 test, 40 docs (the design doc). feat-type touching core, so no size block; below both the 500-line maintainer-awareness and the 1000-line split thresholds. Breadth is 18 files across two packages, which is a threading cost rather than a red flag on its own.

Approach — what I'd want answered before this goes further. I skimmed the code closely enough to have opinions, so here they are as questions rather than as a block:

  • Ordering: the new flattening runs before the adapter clamp, and it drops tiers the clamp would have mapped. applyConfiguredReasoningEffort is applied to baseRequest in buildRequest, i.e. before this.config.provider.buildRequest(...) at pipeline.ts:880. It destructures effort out of the nested reasoning object unconditionally and only promotes it to top-level when the tier is in the configured efforts list. For deepseek-v4-pro (efforts: ['high','max']), a stored xhigh therefore reaches the DeepSeek adapter with no nested effort → translateReasoningEffort no-ops → the request carries no reasoning_effort at all and DeepSeek falls back to its own default, where main today sends max. Same for low/medium, which the adapter maps to high. clampConfiguredReasoningEffort never sees the value — and its docstring says it exists precisely because "the tier is configured once (/effort) and persisted, so an unaccepted value is not a one-off rejection". /effort and the ACP path do reject out-of-list tiers now, but setReasoningEffort itself does not validate, and priorReasoningEffort is restored across auth/model switches (config.ts:4267, 5486, 5503), so a tier persisted before an upgrade is a live path. Routing the configured list through the existing clampReasoningEffort instead of dropping would keep the invariant and reuse code that is already tested.
  • A partial capability in user settings crashes /effort. getReasoningEffortsForConfig returns reasoning.efforts when toggleOnly is falsy. modelProviders is not schema-validated (settingsSchema.ts:359 is a bare type: 'object'; validateModelConfig only checks id and the selector-only flags), so a hand-written capabilities: { reasoning: { thinking: true } } — no toggleOnly, no efforts — yields undefined from a function typed readonly ReasoningEffort[]. effort-command.ts then calls availableTiers.join(', ') and dialogs-modes.tsx spreads [...tiers], so every /effort invocation and the OpenTUI dialog render throw a TypeError. Since the design doc makes this a configuration-only surface users are meant to edit, a runtime guard (reasoning.efforts ?? []) seems worth having.
  • canDisable?: false is read in four places and set by no producer — neither the DeepSeek preset nor any of the six static entries. It is inert today; is it needed now, or is it a knob for the deferred follow-up?
  • disableField is required by the new core type but only read in the core pipeline (registry path). The six values added to the CLI's static MODEL_CONFIGURATIONS table are never read on that path — they exist to satisfy the shared type. Worth deciding whether the CLI's static table and the core registry should really share one wire-level type when only one of the two paths reaches the wire.
  • The diff is wider than "only deepseek-v4-pro". Because disableField is mandatory, five existing toggle-only Qwen models changed too, and with them the TUI: /effort on e.g. qwen3.7-plus no longer opens a picker (it returns an info message), and /effort high now errors where it previously set the tier. That may well be the intended "Before" fix, but it is a user-visible narrowing on this product's primary models and it is not in Risk & Scope, which says "Breaking changes / migration notes: none".
  • buildGenerateContentConfig's samplingParams early-return now merges buildReasoningConfig(request). That changes the wire for every model whose registry entry carries samplingParamsprovider-config.ts:106 emits it from an advanced maxTokens config — not just DeepSeek. It looks like a genuine consistency fix (that branch used to drop a configured reasoning object entirely), but it deserves a line under Risk & Scope and ideally a test of its own.
  • Layering: effort-command.ts, DialogManager.tsx and dialogs-modes.tsx now import from acp-integration/model-configuration.js. There is one precedent (modelCommand.ts), so this is a question, not a violation — but getReasoningEffortsForConfig reads like core material, next to clampReasoningEffort.

Risk: the revert-history screen matched four high-risk paths — acp-integration/acpAgent.ts, acp-integration/model-configuration.ts, acp-integration/session/Session.ts, and core/openaiContentGenerator/pipeline.ts. That is not a block, but it means full review depth and real CI evidence before anyone approves.

Testing: CI on the reviewed commit was still in flight when this ran — Test (ubuntu-latest, Node 22.x), Lint & Static, Serve A/B, Integration Tests (no-AK, No Sandbox), OpenTUI no-flicker gate, TUI parity snapshots, Real daemon E2E / Java 11 and Dependency CVE audit all in_progress, with Test (macos/windows) and Integration Tests (CLI, No Sandbox) skipped. So there is no test evidence to quote yet, and nothing here is verified by me — this pass is static reading only. Two things the diff cannot settle, if a maintainer wants them settled: @qwen-code /verify for the wire claims (top-level reasoning_effort: "max", thinking: { "type": "disabled" }, and specifically what a stored xhigh produces before vs after — the new pipeline test injects getResolvedModelConfig as a mock, so it passes whether the out-of-list tier is clamped or dropped), and @qwen-code /tmux for the two effort pickers.

Not moving on to code review as a separate stage: the direction question above is the thing to settle first, and I would rather hand it over with the concerns named than approve or reject around it. No approval and no request-changes from me — this needs a human call. I could not resolve an accountable owner deterministically (the PR carries no area label, there is no prior human reviewer, and no maintainer handle is configured), so no @mention: whoever owns model/provider config, this one is for you.

中文说明

感谢贡献 —— PR 描述和设计文档把意图讲得很清楚,读起来很顺。

模板完整 ✓(必需标题齐全,Tested-on 已填写,含中文说明)。

**问题:**这是 feat,不要求复现;背后的 UX 观察是真实的 —— 今天 /effortdeepseek-v4-pro 上会提供该模型并不接受的 low/medium/xhigh。但"新增 reasoning 支持必须靠散落在客户端和请求适配器里的模型名/hostname 分支"这个前提与 main 上的现状不符。provider 适配层已经存在一套按模型声明的契约:

  • OpenAICompatibleProvider.supportedReasoningEffortsFor(model),其注释就是"该 endpoint 对 model 接受的 effort 档位;接受 max 的子类可覆盖"。deepseek.tszai.ts 已覆盖,DashScope 声明了 DASHSCOPE_TIERED_EFFORTS
  • clampConfiguredReasoningEffort() 通过 core/reasoning-effort.tsclampReasoningEffort(requested, supported) 收敛档位,该文件头部写明这套阶梯的存在就是"让新 provider 只需声明自己支持的子集"。
  • deepseek.tstranslateReasoningEffort() 已经把嵌套 reasoning.effort 拍平成顶层 reasoning_effort(含 xhigh → max),pipeline 也已对 DeepSeek 域名发送 thinking: { type: 'disabled' }
  • Config.setReasoningEffort 的注释直接写了这条不变量:"Provider adapters clamp the tier to what the active model supports."

所以真正要回答的不是"模型该不该声明 reasoning 能力"(该),而是这份声明放在哪里:provider preset / 模型注册表(本 PR),还是 provider 适配器(现状)。本 PR 之后两套机制同时存在,而且结论互相冲突。

**方向:转交 maintainer。**这触及模型选择、新增了用户可编辑的 settings 契约(modelProviders[].capabilities.reasoning),并改变了 ACP reasoning_effort 会话配置项的形状(选项列表与 _meta)。这些属于 gate 不能自行决定架构的领域。同时核心改动是 packages/core/src/{models,providers,core} 内 105 行生产代码,适用核心 100% 信心门槛 —— 而基于下面的具体问题,我没有到 100%。

**规模:**共 534 行改动 → 生产 368 行(core 105、cli 263)、测试 126 行文档 40 行(设计文档)。feat 类型触及核心,不做规模拦截;低于 500 行维护者关注阈值与 1000 行拆分建议阈值。跨两个包 18 个文件属于穿透成本,本身不是危险信号。

**方案 —— 继续推进前希望先有答案的点。**我把代码看得足够细,所以直接以问题形式提出,而不是当作拦截:

  • 顺序问题:新的拍平逻辑跑在适配器 clamp 之前,并且丢弃了 clamp 本可以映射的档位。applyConfiguredReasoningEffort 作用在 buildRequestbaseRequest 上,早于 pipeline.ts:880 的 this.config.provider.buildRequest(...)。它无条件把 effort 从嵌套 reasoning 中取出,只有当档位在配置的 efforts 列表内才提升为顶层字段。对 deepseek-v4-proefforts: ['high','max']),已存储的 xhigh 到达 DeepSeek 适配器时嵌套 effort 已被删掉 → translateReasoningEffort 空转 → 请求完全不带 reasoning_effort,DeepSeek 回落到自身默认;而今天的 main 会发 maxlow/medium 同理(适配器本会映射为 high)。clampConfiguredReasoningEffort 根本看不到这个值 —— 而它的注释写明它存在正是因为"档位由 /effort 设置一次并被持久化,不被接受的值不是一次性拒绝"。/effort 与 ACP 路径现在确实会拒绝列表外档位,但 setReasoningEffort 自身不校验,且 priorReasoningEffort 会在 auth/模型切换时被恢复(config.ts:4267、5486、5503),所以升级前持久化的档位是现实路径。把配置列表接到已有的 clampReasoningEffort 上(而不是丢弃)既能保住不变量,也能复用已有测试覆盖的代码。
  • settings 里不完整的能力声明会让 /effort 崩溃。getReasoningEffortsForConfigtoggleOnly 为假时返回 reasoning.effortsmodelProviders 没有 schema 校验(settingsSchema.ts:359 只是 type: 'object'validateModelConfig 只检查 id 与 selector-only 标志),因此手写 capabilities: { reasoning: { thinking: true } }(既无 toggleOnly 也无 efforts)会让一个声明为 readonly ReasoningEffort[] 的函数返回 undefinedeffort-command.ts 随即调用 availableTiers.join(', ')dialogs-modes.tsx 展开 [...tiers],于是每次 /effort 与 OpenTUI 弹窗渲染都会抛 TypeError。既然设计文档把它定位为用户应当编辑的"仅配置"入口,加一层运行时兜底(reasoning.efforts ?? [])看起来是必要的。
  • canDisable?: false 有四处读取,却没有任何生产者写入 —— DeepSeek preset 和六个静态条目都没设。它今天是惰性的;是现在就需要,还是为后续 PR 预留的开关?
  • **disableField 被新的 core 类型设为必填,但只有 core pipeline(注册表路径)读取它。**CLI 静态 MODEL_CONFIGURATIONS 表新增的六个值在该路径上永不被读取,只是为了满足共享类型。CLI 静态表与 core 注册表是否真的应该共用一个"含 wire 字段"的类型,值得定一下 —— 毕竟只有一条路径真正走到 wire。
  • **diff 比"只有 deepseek-v4-pro"要宽。**因为 disableField 必填,五个既有 toggle-only Qwen 模型也被改动,TUI 随之变化:例如 qwen3.7-plus/effort 不再打开选择器(改为返回提示信息),/effort high 由"设置成功"变成报错。这也许正是 PR 描述里的 "Before" 修复,但它是本产品主力模型上的用户可见收窄,而 Risk & Scope 写的是"破坏性变更 / 迁移:无"。
  • **buildGenerateContentConfigsamplingParams 提前返回分支现在会合并 buildReasoningConfig(request)。**这改变了所有注册表条目带 samplingParams 的模型的 wire 行为(provider-config.ts:106 会因 advanced maxTokens 生成它),不只是 DeepSeek。看起来是合理的一致性修复(该分支过去会把配置好的 reasoning 整个丢掉),但值得在 Risk & Scope 里写一句,最好也补一个针对性测试。
  • 分层:effort-command.tsDialogManager.tsxdialogs-modes.tsx 现在从 acp-integration/model-configuration.js 导入。已有一处先例(modelCommand.ts),所以这是问题而非违规 —— 但 getReasoningEffortsForConfig 更像是 core 的东西,应该放在 clampReasoningEffort 旁边。

**风险:**回滚历史筛查命中四个高风险路径 —— acp-integration/acpAgent.tsacp-integration/model-configuration.tsacp-integration/session/Session.tscore/openaiContentGenerator/pipeline.ts。这不是拦截,但意味着任何人批准之前都需要完整 review 深度和真实 CI 证据。

**测试:**本次运行时该 commit 的 CI 仍在进行 —— Test (ubuntu-latest, Node 22.x)Lint & StaticServe A/BIntegration Tests (no-AK, No Sandbox)OpenTUI no-flicker gateTUI parity snapshotsReal daemon E2E / Java 11Dependency CVE audit 均为 in_progressTest (macos/windows)Integration Tests (CLI, No Sandbox) 为 skipped。因此目前没有可引用的测试证据;本轮全部是静态阅读,未验证任何运行时行为。若 maintainer 想把两点落实:@qwen-code /verify 验证 wire 结论(顶层 reasoning_effort: "max"thinking: { "type": "disabled" },尤其是已存储的 xhigh 在改动前后的实际结果 —— 新增的 pipeline 测试把 getResolvedModelConfig 作为 mock 注入,因此无论列表外档位是被 clamp 还是被丢弃,它都会通过),@qwen-code /tmux 验证两个 effort 选择器。

不再单独进入代码审查阶段:上面这个方向问题应当先定,与其绕过它去批准或拒绝,不如把顾虑讲清楚交出去。我没有批准,也没有 request changes —— 这一条需要人来定。我无法确定地解析出责任人(PR 没有 area 标签、没有既有的人类 reviewer、也没有配置 maintainer handle),所以没有 @:负责 model/provider 配置的维护者,这一条交给你。

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 22f6cb11844e3e9d41eea32d7d63c0c42e6ecc90 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head 22f6cb1, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

No response changes against the PR base across 12 scenario(s).

Qwen Code · serve A/B

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants